package com.xingchi.tornado.springdoc.config;


import com.xingchi.tornado.springdoc.anno.ApiVersion;
import com.xingchi.tornado.springdoc.model.PackageGroup;
import io.swagger.v3.oas.models.ExternalDocumentation;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import org.apache.commons.lang3.StringUtils;
import org.springdoc.core.GroupedOpenApi;
import org.springdoc.core.filters.OpenApiMethodFilter;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Controller;
import org.springframework.util.ClassUtils;
import org.springframework.web.bind.annotation.RestController;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import static org.springdoc.core.Constants.ALL_PATTERN;
import static org.springdoc.core.Constants.DEFAULT_GROUP_NAME;
import static org.springdoc.core.Constants.SPRINGDOC_ENABLED;

/**
 * 自动配置
 *
 * @author xiaoya
 * @date 2022/12/5 14:58
 * @modified xiaoya
 */
@Order
@Configuration
@EnableConfigurationProperties(OpenApiProperties.class)
@ConditionalOnProperty(name = SPRINGDOC_ENABLED, havingValue = "true")
public class OpenApiAutoConfiguration {

    /**
     * 基于注解方式进行分组
     */
    public static final String ANNOTATION_OPENAPI_GROUP_BEAN_NAME_PREFIX = "annotationOpenApiGroup";

    /**
     * 基于包扫描方式进行分组
     */
    public static final String PACKAGE_OPENAPI_GROUP_BEAN_NAME_PREFIX = "packageOpenApiGroup";

    @Autowired
    private WebEndpointProperties webEndpointProperties;

    @Bean
    public OpenAPI openAPI(OpenApiProperties openApiProperties) {
        return new OpenAPI()
                .info(getApiInfo(openApiProperties))
                .externalDocs(new ExternalDocumentation()
                        .description(openApiProperties.getExternalDocumentation().getDescription())
                        .url(openApiProperties.getExternalDocumentation().getUrl())
                        .extensions(openApiProperties.getContact().getExtensions()));
    }

    private Info getApiInfo(OpenApiProperties openApiProperties) {

        OpenApiProperties.Contact contact = openApiProperties.getContact();

        Contact finalContact = new Contact();
        finalContact.setName(contact.getName());
        finalContact.setEmail(contact.getEmail());
        finalContact.setUrl(contact.getUrl());
        finalContact.setExtensions(contact.getExtensions());

        OpenApiProperties.License license = openApiProperties.getLicense();
        License finalLicense = new License();
        finalLicense.setName(license.getName());
        finalLicense.setUrl(license.getUrl());
        finalLicense.setExtensions(license.getExtensions());

        return new Info()
                .title(openApiProperties.getTitle())
                .version(openApiProperties.getVersion())
                .description(openApiProperties.getDescription())
                .license(finalLicense)
                .contact(finalContact);
    }

    /**
     * 创建默认分组
     *
     * @return      默认分组
     */
    @Bean
    @ConditionalOnMissingBean
    public GroupedOpenApi getDefaultGroupOpenApi(Environment environment) {
        Binder binder = Binder.get(environment);

        OpenApiProperties properties = binder.bind("springdoc.tornado.doc", OpenApiProperties.class).orElse(null);
        String defaultGroupName;
        if (properties == null) {
            defaultGroupName = DEFAULT_GROUP_NAME;
        } else {
            defaultGroupName = properties.getDefaultGroupName();
        }

        return GroupedOpenApi.builder()
                .group(defaultGroupName)
                .displayName(defaultGroupName)
                .pathsToMatch(ALL_PATTERN)
                .pathsToExclude("/actuator" + ALL_PATTERN, "/health/**")
                .build();
    }

    @Bean
    @Order
    public BeanDefinitionRegistryPostProcessor beanDefinitionRegistryPostProcessor(Environment environment) {
        return new BeanDefinitionRegistryPostProcessor() {
            @Override
            public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
                Binder binder = Binder.get(environment);

                int index = 0;
                List<PackageGroup> packageGroups = binder.bind("springdoc.tornado.doc.group", Bindable.listOf(PackageGroup.class)).orElse(Collections.emptyList());
                for (PackageGroup packageGroup : packageGroups) {
                    String groupName = PACKAGE_OPENAPI_GROUP_BEAN_NAME_PREFIX + index++;
                    BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(GroupedOpenApi.class, () -> {
                        return GroupedOpenApi.builder()
                                .group(groupName)
                                .displayName(packageGroup.getGroupName())
                                .pathsToMatch(packageGroup.getPackageName())
                                .build();
                    });
                    beanDefinitionBuilder.setLazyInit(true);
                    registry.registerBeanDefinition(groupName, beanDefinitionBuilder.getBeanDefinition());
                }

                String[] beanDefinitionNames = registry.getBeanDefinitionNames();
                Set<String> groupNames = new HashSet<>();
                for (String beanName : beanDefinitionNames) {
                    BeanDefinition beanDefinition = registry.getBeanDefinition(beanName);
                    String beanClassName = beanDefinition.getBeanClassName();
                    if (beanClassName == null) {
                        continue;
                    }
                    Class<?> resolve = null;
                    try {
                        resolve = ClassUtils.forName(beanClassName, this.getClass().getClassLoader());
                    } catch (ClassNotFoundException e) {
                        continue;
                    }

                    if (!(resolve.isAnnotationPresent(Controller.class) || resolve.isAnnotationPresent(RestController.class))) {
                        continue;
                    }
                    Method[] methods = resolve.getMethods();
                    for (Method method : methods) {
                        if (method.isAnnotationPresent(ApiVersion.class)) {
                            // 处理标注了YourAnnotation注解的方法
                            ApiVersion annotation = method.getAnnotation(ApiVersion.class);
                            String[] group = annotation.value();
                            groupNames.addAll(Arrays.asList(group));
                        }
                    }
                }

                List<String> sortGroupNames = groupNames.stream().sorted().collect(Collectors.toList());
                index = 0;
                for (String groupName : sortGroupNames) {
                    String name = ANNOTATION_OPENAPI_GROUP_BEAN_NAME_PREFIX + index++;
                    BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(GroupedOpenApi.class, () -> GroupedOpenApi.builder()
                            .group(name)
                            .displayName(groupName)
                            .addOpenApiMethodFilter(method -> {
                                ApiVersion annotation = method.getAnnotation(ApiVersion.class);
                                if (annotation == null) {
                                    return false;
                                }
                                String[] group = annotation.value();
                                return Arrays.asList(group).contains(groupName);
                            })
                            .build());
                    beanDefinitionBuilder.setLazyInit(true);
                    registry.registerBeanDefinition(name, beanDefinitionBuilder.getBeanDefinition());
                }

            }

            @Override
            public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {

            }
        };
    }
}
